#!/usr/bin/python
#
# Copyright 2009 Vidoop LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Library that provides a Python interface to the VidoopSecure API"""

__author__ = 'michael.richardson@vidoop.com'
__version__ = '0.1'

import httplib
import urllib
import xml.dom.minidom


class VidoopSecureException(Exception):
    """Generic VidoopSecure exception

    VidoopSecureException works like Exception, except that it has a
    format variable that is used for converting to a string.
    >>> vse = VidoopSecureException('The %(service)s service failed: %(err)s')
    >>> vse.service = 'captcha'
    >>> vse.err = '"I don't like you"'
    >>> print vse
    The captcha service failed: "I don't like you"
    """

    def __init__(self, format=None, raw_message=None):
        """Initialize a VidoopSecureException

        Keyword Arguments:
        format - A format string using named keywords. Optional
        raw_message - The error message returned by the webservice. Optional.
        """
        if format is None:
            self.format = 'Generic VidoopSecure Exception'
            if raw_message is not None:
                self.format += ': %(raw_message)s'
        else:
            self.format = format
        if raw_message is not None:
            self.raw_message = raw_message

    def __str__(self):
        """String format a VidoopSecureException"""
        try:
            return self.format % self.__dict__
        except KeyError:
            return self.format + " (with VidoopSecureException keyword failure)"


class ServiceNotEnabled(VidoopSecureException):
    """The service is not enabled for the site."""

    def __init__(self, service):
        VidoopSecureException.__init__(self,
            "The service '%(service)s' is not enabled for your site.")
        self.service = service


class RequestPending(VidoopSecureException):
    """The request is still pending."""

    def __init__(self, service, transaction):
        VidoopSecureException.__init__(self,
            "The %(service)s request '%(transaction)s' is still pending.")
        self.service = service
        self.transaction = transaction


class InvalidPhoneNumber(VidoopSecureException):
    """The VidoopSecure API can't handle this phone number."""

    def __init__(self, phone):
        VidoopSecureException.__init__(self, "Sorry, we can't call '%(phone)s'.")
        self.phone = phone


class VoicePINNotFound(VidoopSecureException):
    """VoicePIN is an invalid voicepin_id"""

    def __init__(self, voicepin_id):
        VidoopSecureException.__init__(self,
            "VoicePIN '%(voicepin_id)s' is an invalid voicepin_id.")
        self.voicepin_id = voicepin_id


class VoicePINExpired(VidoopSecureException):
    """VoicePIN has expired"""

    def __init__(self, voicepin_id):
        VidoopSecureException.__init__(self,
            "VoicePIN '%(voicepin_id)s' has expired.")

        self.voicepin_id = voicepin_id


class VoicePINFailed(VidoopSecureException):
    """VoicePIN has failed"""

    def __init__(self, voicepin_id):
        VidoopSecureException.__init__(self,
            "VoicePIN '%(voicepin_id)s' has failed.")

        self.voicepin_id = voicepin_id


class VoicePINRejected(VidoopSecureException):
    """VoicePIN has been canceled by the user"""

    def __init__(self, voicepin_id):
        VidoopSecureException.__init__(self,
            "VoicePIN '%(voicepin_id)s' has been canceled by the user.")
        self.voicepin_id = voicepin_id


class VoicePTLNotFound(VidoopSecureException):
    """VoicePTL was not found"""

    def __init__(self, voiceptl_id):
        VidoopSecureException.__init__(self,
            "VoicePTL '%(voiceptl_id)s' is an invalid voiceptl_id.")
        self.voiceptl_id = voiceptl_id


class VoicePTLExpired(VidoopSecureException):
    """This VoicePTL request has expired"""

    def __init__(self, voiceptl_id):
        VidoopSecureException.__init__(self,
            "VoicePTL '%(voiceptl_id)s' has expired.")
        self.voiceptl_id = voiceptl_id


class VoicePTLFailed(VidoopSecureException):
    """This VoicePTL request failed"""

    def __init__(self, voiceptl_id):
        VidoopSecureException.__init__(self,
            "VoicePTL '%(voiceptl_id)s' has failed.")
        self.voiceptl_id = voiceptl_id


class VoicePTLCanceled(VidoopSecureException):
    """This VoicePTL request has been canceled by the user"""

    def __init__(self, voiceptl_id):
        VidoopSecureException.__init__(self,
            "VoicePTL '%(voiceptl_id)s' has been canceled by the user.")
        self.voiceptl_id = voiceptl_id


class SMSOTPErrorSending(VidoopSecureException):
    """Generic SMS sending error"""

    def __init__(self, phone=None):
        format = "Sorry, we could not send an SMS message"
        if phone is not None:
            self.phone = phone
            format += " to %(phone)s."
        else:
            format += ".."
        VidoopSecureException.__init__(self, format)


class SMSOTPNotFound(VidoopSecureException):
    """SMSOTP was not found"""

    def __init__(self, smsotp_id):
        VidoopSecureException.__init__(self,
            "SMS OTP '%(smsotp_id)s' is an invalid smsotp_id.")
        self.smsotp_id = smsotp_id


class SMSOTPExpired(VidoopSecureException):
    """This SMSOTP request has expired"""

    def __init__(self, smsotp_id):
        VidoopSecureException.__init__(self,
            "SMS OTP '%(smsotp_id)s' has expired.")
        self.smsotp_id = smsotp_id


class SMSOTPAlreadyAuthed(VidoopSecureException):
    """This SMSOTP request has already been completed"""

    def __init__(self, smsotp_id):
        VidoopSecureException.__init__(self,
            "SMS OTP '%(smsotp_id)s' has already been completed.")
        self.smsotp_id = smsotp_id


class SMSOTPFailed(VidoopSecureException):
    """Invalid validation code for SMS OTP"""

    def __init__(self, smsotp_id):
        VidoopSecureException.__init__(self,
            "Invalid validation code for SMS OTP '%(smsotp_id)s'.")
        self.smsotp_id = smsotp_id


class VoiceOTPErrorSending(VidoopSecureException):
    """Generic voice sending error"""

    def __init__(self, phone=None):
        format = "Sorry, we could not call "
        if phone is None:
            format += "that number."
        else:
            self.phone = phone
            format += "'%(phone)s'."
        VidoopSecureException.__init__(self, format)


class VoiceOTPNotFound(VidoopSecureException):
    """"VoiceOTP was not found"""

    def __init__(self, voiceotp_id):
        VidoopSecureException.__init__(self,
            "Voice OTP '%(voiceotp_id)s' is an invalid voiceotp_id.")
        self.voiceotp_id = voiceotp_id


class VoiceOTPAlreadyAuthed(VidoopSecureException):
    """This VoiceOTP has already been authorized"""

    def __init__(self, voiceotp_id):
        VidoopSecureException.__init__(self,
            "Voice OTP '%(voiceotp_id)s' has already been attempted.")
        self.voiceotp_id = voiceotp_id


class VoiceOTPExpired(VidoopSecureException):
    """This VoiceOTP has expired"""

    def __init__(self, voiceotp_id):
        VidoopSecureException.__init__(self,
            "Voice OTP '%(voiceotp_id)s' has expired.")
        self.voiceotp_id = voiceotp_id


class VoiceOTPFailed(VidoopSecureException):
    """Invalid validation code for Voice OTP"""

    def __init__(self, voiceotp_id):
        VidoopSecureException.__init__(self,
            "Invalid validation code for '%(voiceotp_id)s'.")
        self.voiceotp_id = voiceotp_id


class CaptchaFailed(VidoopSecureException):
    """Captcha attempt failed"""

    def __init__(self, captcha_id):
        VidoopSecureException.__init__(self,
            "Invalid code for CAPTCHA '%(captcha_id)s'.")
        self.captcha_id = captcha_id


class CaptchaExpired(VidoopSecureException):
    """Captcha has expired"""

    def __init__(self, captcha_id):
        VidoopSecureException.__init__(self,
            "CAPTCHA '%(captcha_id)s' has expired.")
        self.captcha_id = captcha_id


class CaptchaNotFound(VidoopSecureException):
    """Captcha not found or missing"""

    def __init__(self, captcha_id):
        VidoopSecureException.__init__(self,
            "CAPTCHA '%(captcha_id)s' is an invalid captcha_id.")
        self.captcha_id = captcha_id


class CaptchaAlreadyTried(VidoopSecureException):
    """Captcha has already been attempted"""

    def __init__(self, captcha_id):
        VidoopSecureException.__init__(self,
            "CAPTCHA '%(captcha_id)s' has already been attempted.")
        self.captcha_id = captcha_id


class ImageshieldFailed(VidoopSecureException):
    """ImageShield attempt failed"""

    def __init__(self, imageshield_id):
        VidoopSecureException.__init__(self,
            "Invalid code for ImageShield '%(imageshield_id)s'.")
        self.imageshield_id = imageshield_id


class ImageshieldExpired(VidoopSecureException):
    """ImageShield has expired"""

    def __init__(self, imageshield_id):
        VidoopSecureException.__init__(self,
            "ImageShield '%(imageshield_id)s' has expired.")
        self.imageshield_id = imageshield_id


class ImageshieldNotFound(VidoopSecureException):
    """ImageShield not found or missing"""

    def __init__(self, imageshield_id):
        VidoopSecureException.__init__(self,
            "ImageShield '%(imageshield_id)s' is an invalid imageshield_id.")
        self.imageshield_id = imageshield_id


class ImageshieldAlreadyTried(VidoopSecureException):
    """ImageShield has already been attempted"""

    def __init__(self, imageshield_id):
        VidoopSecureException.__init__(self,
            "ImageShield '%(imageshield_id)s' has already been attempted.")
        self.imageshield_id = imageshield_id


class VidoopSecure(object):

    def __init__(self, site_username, site_pass, customer_name, site_name,
            vs_server='api.vidoop.com', vs_port=443):
        self.vs_user = site_username
        self.vs_pass = site_pass
        self.customer_name = customer_name
        self.site_name = site_name
        self.vs_server = vs_server
        self.vs_port = int(vs_port)

    def _raise_generic_error(self, error_string):
        raise VidoopSecureException(raw_message=error_string)

    def _build_request(self, endpoint):
        return '/vs/customers/%s/sites/%s/services/%s' % (self.customer_name,
            self.site_name, endpoint)

    def _get_response(self, endpoint, method, params=None, _build_url=True,
            accept_language=None):
        """Performs a http request on the webservice"""

        if params is None:
            params = {}

        if self.vs_port == 443:
            conn = httplib.HTTPSConnection(self.vs_server, self.vs_port)
        else:
            conn = httplib.HTTPConnection(self.vs_server, self.vs_port)
        auth_string = ("%s:%s" % (self.vs_user,
            self.vs_pass)).encode('base64').replace('\n', '')
        headers = {"Authorization": "Basic %s" % auth_string}
        if accept_language:
            headers['Accept-Language'] = accept_language
        if method in ['POST', 'PUT']:
            headers['Content-Type'] = 'application/x-www-form-urlencoded'

        # utf-8 encode the params that can be utf-8 encoded
        data = {}
        for k, v in params.iteritems():
            try:
                data[k] = v.encode('utf-8')
            except:
                data[k] = v

        params = urllib.urlencode(data)
        if _build_url:
            request = self._build_request(endpoint)
        else:
            request = endpoint
        conn.request(method, request, params, headers=headers)
        return conn.getresponse()

    def start_voiceptl(self, user_id, phone):
        """Initiate a VoicePTL transaction.  Return the VoicePTL ID."""
        endpoint = 'voiceptl'
        params = {
            'user_id': user_id,
            'phone': phone,
        }
        response = self._get_response(endpoint, 'POST', params)
        if response.status == 400:
            raise InvalidPhoneNumber(phone)
        if response.status == 404:
            raise ServiceNotEnabled("voiceptl")
        if response.status != 201:
            self._raise_generic_error(response.read())
        vs_xml = xml.dom.minidom.parseString(response.read())
        return vs_xml.getElementsByTagName('id')[0].firstChild.data

    def verify_voiceptl(self, voiceptl_id):
        """Check the status of a VoicePTL request identified by voiceptl_id.

        Return None if the request is pending
        Return True if the request has completed
        Raise VoicePTLFailed if the VoicePTL request failed
        Raise VoicePTLExpired exception if the request has expired
        Raise VoicePTLCanceled exception if the user has canceled the request

        """
        endpoint = 'voiceptl/%s' % voiceptl_id
        response = self._get_response(endpoint, 'GET')
        if response.status == 410:
            raise VoicePTLExpired(voiceptl_id)
        if response.status == 404:
            raise VoicePTLNotFound(voiceptl_id)
        if response.status != 200:
            return self._raise_generic_error(response.read())
        vs_xml = xml.dom.minidom.parseString(response.read())
        send_status = vs_xml.getElementsByTagName(
            'send_status')[0].firstChild.data
        if send_status == 'failure':
            raise VoicePTLFailed(voiceptl_id)
        elif send_status == 'pending':
            return None
        auth_status = vs_xml.getElementsByTagName(
            'authentication_status')[0].firstChild.data
        if auth_status == 'failure':
            raise VoicePTLCanceled(voiceptl_id)
        elif auth_status == 'pending':
            return None
        elif auth_status == 'success':
            return True

    def start_voicepin(self, user_id, phone, pin):
        """Initiate a VoicePIN transaction.  Return the VoicePIN ID."""
        endpoint = 'voicepin'
        params = {
            'user_id': user_id,
            'phone': phone,
            'pin': pin,
        }
        response = self._get_response(endpoint, 'POST', params)
        if response.status == 400:
            raise InvalidPhoneNumber(phone)
        if response.status == 404:
            raise ServiceNotEnabled("voicepin")
        if response.status != 201:
            return self._raise_generic_error(response.read())
        vs_xml = xml.dom.minidom.parseString(response.read())
        return vs_xml.getElementsByTagName('id')[0].firstChild.data

    def verify_voicepin(self, voicepin_id):
        """Check the status of a VoicePIN request identified by voicepin_id.

        Return None if the request is pending
        Return True if the request has completed
        Raise VoicePINExpired if the request has expired
        Raise VoicePINRejected if the user has rejected the request
        Raise VoicePINFailed if the request has failed

        """
        endpoint = 'voicepin/%s' % voicepin_id
        response = self._get_response(endpoint, 'GET')
        if response.status == 410:
            raise VoicePINExpired(voicepin_id)
        if response.status == 404:
            raise VoicePINNotFound(voicepin_id)
        if response.status != 200:
            return self._raise_generic_error(response.read())
        vs_xml = xml.dom.minidom.parseString(response.read())
        send_status = vs_xml.getElementsByTagName(
            'send_status')[0].firstChild.data
        auth_status = vs_xml.getElementsByTagName(
            'authentication_status')[0].firstChild.data
        if send_status == 'pending':
            return None
        if 'failure' in [send_status, auth_status]:
            raise VoicePINFailed(voicepin_id)
        if auth_status == 'rejected':
            raise VoicePINRejected(voicepin_id)
        elif auth_status == 'success':
            return True
        elif auth_status == 'pending':
            return None

    def start_smsotp(self, user_id, phone):
        """Initiate an SMSOTP request.  Return the SMSOTP ID."""
        endpoint = 'smsotp'
        params = {
            'user_id': user_id,
            'phone': phone,
        }
        response = self._get_response(endpoint, 'POST', params)
        if response.status == 404:
            raise ServiceNotEnabled("smsotp")
        if response.status != 201:
            raise SMSOTPErrorSending(phone)
        vs_xml = xml.dom.minidom.parseString(response.read())
        return vs_xml.getElementsByTagName('id')[0].firstChild.data

    def validate_smsotp(self, smsotp_id, code, phone=None):
        """Check SMSOTP smsotp_id request with code.

        Return True if the request has been completed
        Raise SMSOTPExpired if the request has expired
        Raise SMSOTPFailed if the request has failed
        Raise SMSOTPAlreadyAuthed if the request has already been completed

        """
        endpoint = 'smsotp/%s' % smsotp_id
        params = {'otp': code}
        response = self._get_response(endpoint, 'POST', params)
        code = response.status
        if code == 200:
            return True
        elif code == 403:
            resp = response.read()
            if 'Failed' in resp:
                raise SMSOTPErrorSending(phone)
            elif 'pending' in resp:
                raise RequestPending('smsotp', smsotp_id)
            else:
                self._raise_generic_error(resp)
        elif code == 404:
            raise SMSOTPNotFound(smsotp_id)
        elif code == 409:
            raise SMSOTPAlreadyAuthed(smsotp_id)
        elif code == 410:
            raise SMSOTPExpired(smsotp_id)
        elif code == 430:
            raise SMSOTPFailed(smsotp_id)
        self._raise_generic_error(response.read())

    def start_voiceotp(self, user_id, phone):
        """Initiate a VoiceOTP request.  Return the VoiceOTP ID."""
        endpoint = 'voiceotp'
        params = {
            'user_id': user_id,
            'phone': phone,
        }
        response = self._get_response(endpoint, 'POST', params)
        if response.status == 400:
            raise VoiceOTPErrorSending(phone)
        if response.status == 404:
            raise ServiceNotEnabled("smsotp")
        if response.status != 201:
            self._raise_generic_error(response.read())
        vs_xml = xml.dom.minidom.parseString(response.read())
        return vs_xml.getElementsByTagName('id')[0].firstChild.data

    def validate_voiceotp(self, voiceotp_id, code, phone=None):
        """Check VoiceOTP voiceotp_id with code.

        Return True if the code was successful
        Raise VoiceOTPExpired if the request has expired
        Raise VoiceOTPAlreadyAuthed if the request has already been attempted
        Raise VoiceOTPExpired if the request has expired
        Raise VoiceOTPFailed if the code is invalid

        """
        endpoint = 'voiceotp/%s' % voiceotp_id
        params = {'otp': code}
        response = self._get_response(endpoint, 'POST', params)
        code = response.status
        if code == 200:
            return True
        elif code == 403:
            resp = response.read()
            if 'Failed' in resp:
                raise VoiceOTPErrorSending(phone)
            elif 'pending' in resp:
                raise RequestPending('voiceotp', voiceotp_id)
            else:
                self._raise_generic_error(resp)
        elif code == 404:
            raise VoiceOTPNotFound(voiceotp_id)
        elif code == 409:
            raise VoiceOTPAlreadyAuthed(voiceptl_id)
        elif code == 410:
            raise VoiceOTPExpired(voiceotp_id)
        elif code == 430:
            raise VoiceOTPFailed(voiceotp_id)
        else:
            self._raise_generic_error(response.read())

    def create_captcha(self, length=3, order_matters=True, width=4, height=3,
            image_code_color='White', accept_language=None):
        """Creates a CAPTCHA.

        Return captcha_id, captcha_url, captcha_categories (list),
            captcha_text

        """
        endpoint = 'captcha'
        params = {
            'captcha_length': length,
            'order_matters': order_matters,
            'width': width,
            'height': height,
            'image_code_color': image_code_color,
        }
        response = self._get_response(endpoint, 'POST', params,
            accept_language=accept_language)
        if response.status == 404:
            raise ServiceNotEnabled("captcha")
        if response.status != 201:
            return self._raise_generic_error(response.read())
        vs_xml = xml.dom.minidom.parseString(response.read())
        captcha_id = vs_xml.getElementsByTagName('id')[0].firstChild.data
        captcha_url = (vs_xml.getElementsByTagName('imageURI')[0]
            .firstChild.data)
        captcha_letters = (vs_xml.getElementsByTagName('letters')[0]
            .firstChild.data)
        captcha_categories = []
        for elem in vs_xml.getElementsByTagName('category_name'):
            captcha_categories.append(elem.firstChild.data)
        captcha_text = vs_xml.getElementsByTagName('text')[0].firstChild.data
        return (captcha_id, captcha_url, captcha_categories, captcha_text,
            captcha_letters)

    def submit_captcha(self, captcha_id, captcha_code,
            accept_language=None):
        """Checks to see if captcha_code is valid for captcha_id.

        Return True if the code is correct
        Raise CaptchaNotFound if captcha_id is invalid
        Raise CaptchaFailed if the code is incorrect
        Raise CaptchaAlreadyTried if this captcha has been attempted
        Raise CaptchaExpired if the captcha has expired

        """
        request = '/vs/captchas/%s' % captcha_id
        params = {'code': captcha_code}
        response = self._get_response(request, 'POST', params,
            _build_url=False, accept_language=accept_language)
        code = response.status
        if code == 200:
            return True
        elif code == 404:
            raise CaptchaNotFound(captcha_id)
        elif code == 409:
            raise CaptchaAlreadyTried(captcha_id)
        elif code == 410:
            raise CaptchaExpired(captcha_id)
        elif code == 430:
            raise CaptchaFailed(captcha_id)
        self._raise_generic_error(response.read())

    def create_imageshield(self, user_id, bundle, length=3, order_matters=True,
            width=4, height=3, image_code_color='Red'):
        """Create an ImageShield

        Return imageshield_id, imageshield_url
        """
        endpoint = 'imageshield'
        params = {
            'user_id': user_id,
            'bundle': bundle,
            'imageshield_length': length,
            'order_matters': order_matters,
            'width': width,
            'height': height,
            'image_code_color': image_code_color,
        }
        response = self._get_response(endpoint, 'POST', params)
        if response.status == 404:
            raise ServiceNotEnabled("imageshield")
        if response.status != 201:
            return self._raise_generic_error(response.read())
        vs_xml = xml.dom.minidom.parseString(response.read())
        imageshield_id = vs_xml.getElementsByTagName('id')[0].firstChild.data
        imageshield_url = vs_xml.getElementsByTagName('imageURI')[0] \
            .firstChild.data

        return imageshield_id, imageshield_url

    def submit_imageshield(self, imageshield_id, imageshield_code):
        """Checks to see if imageshield_code is valid for imageshield_id

        Return True if the code is correct
        Raise ImageshieldNotFound if imageshield_id is invalid
        Raise ImageshieldFailed if the code is incorrect
        Raise ImageshieldAlreadyTried if this ImageShield has been attempted
        Raise ImageshieldExpired if the ImageShield has expired
        """

        request = '/vs/imageshields/%s' % imageshield_id
        params = {'code': imageshield_code}
        response = self._get_response(request, 'POST', params,
            _build_url=False)
        code = response.status
        if code == 200:
            return True
        if code == 404:
            raise ImageshieldNotFound(imageshield_id)
        if code == 409:
            raise ImageshieldAlreadyTried(imageshield_id)
        if code == 410:
            raise ImageshieldExpired(imageshield_id)
        if code == 430:
            raise ImageshieldFailed(imageshield_id)
        self._raise_generic_error(response.read())
